<?php

/**
 * @copyright Michiel Hakvoort 2010
 * @license http://www.opensource.org/licenses/bsd-license.php New BSD
 * @package mangrove
 * @subpackage groove
 * @filesource
 */

/*
 * Copyright (c) 2010 Michiel Hakvoort
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. The name of the author may not be used to endorse or promote products
 *    derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
 * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
 * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */


namespace mg;

include(dirname(__FILE__).DS.'functions.php');

final class Groove implements ViewProvider, View {

    private $templetPath    = null;
    private $root             = null;
    private $compileRoot      = null;

    private $properties		= null;

    private $logger			= null;

    // id -> object*
    private $ruleStacks      = null;

    // id -> class+
    private $rules           = null;

    // class -> id+
    private $updateRules     = null;

    // class -> id+
    private $productionRules = null;

    // id -> function
    private $functions       = null;

    // function -> file
    private $files           = null;

    // file -> array(classname)
    private $fileDependencies = null;

    // file -> bool
    private $loadedFiles     = array();

    // rule.class -> bool
    private $classDictionary = null;

    private $depth = 0;
    private $stack = array();

    // Experimental dynamic programming feature
    private $ruleKey = '';
    private $ruleCache = null;

    /**
     *
     * @var GrooveContext
     */
    private $grooveContext = null;

    private $outputBuffer = null;

    // TODO, allow more than one folder as the template path
    public function __construct($templetPath) {
        $this->templetPath = $templetPath;

        $this->outputBuffer = new OutputBuffer();

        $this->grooveContext = get('mg\GrooveContext');

        $runtime = in(function(Runtime $runtime) { return $runtime; } );

        $uniqueId = md5($templetPath);

        $this->root = $runtime->getResource("mangroove.{$uniqueId}");
        $this->compileRoot = $runtime->getResource("mangroove.{$uniqueId}.compiled");

        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; });

        $classDictionary = $storageManager->getStorage("mangroove.{$uniqueId}.classdictionary", true);

        $ruleCache = $storageManager->getStorage("mangroove.{$uniqueId}.rulecache", true);
        $properties = $storageManager->getStorage("mangroove.{$uniqueId}.properties", true);

        $this->ruleCache = $storageManager->cache($ruleCache);

        // TODO reenable class dictionary later

        //		$this->classDictionary = $storageManager->cache($classDictionary);

        $this->properties = $storageManager->cache($properties);

        if($runtime->isTransactional()) {
            $this->rebuild();
        }

        $this->loadRules();
    }
    /*
     public function render($object) {
     return null;
     // Push the object on the stack
     // Create the rule 'string'
     // See whether the rule is cached SHM
     //  by the object type, get all rules who have anything to do with this object LOCAL
     //    if the rule is not complete (count($rule) != count($otherRule)) LOCAL
     //      -> search for the distance between [requiredclass & currentclass] SHM
     //        if the distance > 0, update the rule stack
     //  get all rules which might render the object LOCAL
     //  check each rule on whether it is completed LOCAL
     //    if a rule is completed
     //      call the rule function (this one creates the proper body)
     //        the rule function calls the render body
     // pop the class from all updated rulestacks
     // pop the class from the stack
     // return
     }

     /*
     public function getVariable($variable) {
     if(isset($this->variables[$variable])) {
     return $this->variables[$variable];
     } else {
     return null;
     }
     }

     public function assign($variable, $value) {
     $this->variables[$variable] = $value;
     }
     */

    public function enter($class) {
        $class = mb_strtolower($class);

        if(!isset($this->updateRules[$class])) {
            return;
        }

        $this->stack[] = null;

        foreach($this->updateRules[$class] as $rule) {
            // if a rule can not be updated (because the rule stack is full)
            if(count($this->ruleStacks[$rule]) + 1 === count($this->rules[$rule])) {
                continue;
            }

            $expected = $this->rules[$rule][count($this->ruleStacks[$rule])];

            if(isset($this->classDictionary["{$expected}.{$class}"])) {
                $updates[] = $rule;
                $this->ruleStacks[$rule][] = $this->depth;
            }
        }

        //array_push($this->stack, $class);
        $this->ruleKey .= '.' . $class;
        $this->depth++;
    }

    public function leave($class) {
        $class = mb_strtolower($class);

        if(!isset($this->updateRules[$class])) {
            return;
        }

        array_pop($this->stack);

        $this->depth--;

        foreach($this->updateRules[$class] as $rule) {
            // if a rule can not be updated (because the rule stack is full)
            $ruleStack =& $this->ruleStacks[$rule];
            if(count($ruleStack) > 0 && $ruleStack[count($ruleStack) - 1] == $this->depth) {
                array_pop($ruleStack);
            }
        }

        //array_push($this->stack, $class);
        $this->ruleKey = mb_substr($this->ruleKey, 0, (-mb_strlen($class))+2);
    }

    public function renderDirect($object) {
        if(is_array($object)) {
            foreach($object as $value) {
                $this->renderDirect($value);
            }
        } elseif(is_object($object)) {
            if($object instanceof Viewable && ($view = $object->getView()) instanceof View && !($view instanceof Groove)) {
                $view->write($object, $this->outputBuffer);
                return;
            }

            $class = mb_strtolower(\mg\get_class($object));

            // The class will not get produced, try to iterate or return
            if(!isset($this->productionRules[$class])) {
                if($object instanceof \Traversable) {
                    foreach($object as $value) {
                        $this->renderDirect($value);
                    }
                }
                return;
            }

            $key = $this->ruleKey . '.' . $class;

            $function = false;
            $renderRule = null;

            // Try to use the cache (disabled)
            if(false && isset($this->ruleCache[$key])) {
                $function = $this->ruleCache[$key];
                if(!isset($this->loadedFiles[$this->files[$function]])) {
                    include($this->files[$function]);
                    $this->loadedFiles[$this->files[$function]] = true;
                }
            } else {
                // Get the updatable rules

                $productions =& $this->productionRules[$class];



                // Find a function
                for($i = 0, $c = count($productions); !$function && $i < $c; $i++) {
                    $rule = $productions[$i];

                    if(count($this->ruleStacks[$rule]) + 1 === count($this->rules[$rule])) {
                        $function = $this->functions[$rule];
                        $renderRule = $rule;
                        $filename = $this->files[$function];
                        if(!isset($this->loadedFiles[$filename])) {

                            include($filename);
                            $this->loadedFiles[$filename] = true;

                            $dependencies = $this->fileDependencies[$filename];

                            foreach($dependencies as $className) {
                                if(!isset($this->grooveContext->dependencies[$className])) {
                                    $this->grooveContext->dependencies[$className] = get($className);
                                }
                            }
                            unset($dependencies);
                        }
                        unset($filename);
                    }
                }
            }

            // If no complete rules are found, but the object is traversable... do so
            if(!$function) {
                if($object instanceof \Traversable) {
                    foreach($object as $value) {
                        $this->renderDirect($value);
                    }
                }

                return;
            }

            $updates = array();

            // issue updates
            if(isset($this->updateRules[$class])) {
                // We're continuing the render, update any other rules
                foreach($this->updateRules[$class] as $rule) {
                    // if a rule can not be updated (because the rule stack is full)
                    if(count($this->ruleStacks[$rule]) + 1 === count($this->rules[$rule])) {
                        continue;
                    }


                    $expected = $this->rules[$rule][count($this->ruleStacks[$rule])];

                    if(isset($this->classDictionary["{$expected}.{$class}"])) {
                        $updates[] = $rule;
                        $this->ruleStacks[$rule][] = $this->depth;
                    }
                }
            }

            $this->stack[] = $object;

            $oldKey = $this->ruleKey;
            $this->ruleKey .= '.' . $class;

            $this->depth++;

            //	$this->ruleCache[$this->ruleKey] = $function;

            // Recursion step
            $function($this, $this->grooveContext, $renderRule, $this->stack, $this->ruleStacks[$renderRule], $object);

            $this->ruleKey = $oldKey;

            foreach($updates as $rule) {
                array_pop($this->ruleStacks[$rule]);
            }

            array_pop($this->stack);

            $this->depth--;
        } else {
            echo $object;
        }

    }

    private function loadRules() {
        $rFile = $this->root . DS . 'rules.mg';

        $rules = include($rFile);

        $this->ruleStacks      =& $rules['ruleStacks'];

        $this->updateRules     =& $rules['updateRules'];
        $this->productionRules =& $rules['productionRules'];

        // TODO move to shm
        $this->classDictionary =& $rules['classDictionary'];
        // rule -> class+
        $this->rules           =& $rules['rules'];

        // rule -> function
        $this->functions       =& $rules['functions'];

        // function -> file
        $this->files           =& $rules['files'];

        $this->fileDependencies =& $rules['fileDependencies'];
    }

    protected function rebuild() {

        $architecture = in(function(Architecture $architecture) { return $architecture; } );

        $storageManager = in(function(StorageManager $storageManager) { return $storageManager; } );
        $fileSystem = in(function(FileSystem $fileSystem) { return $fileSystem; } );

        $classUpdates = $architecture->getUpdates();

        $hasUpdate = false;

        /* @var $update ClassUpdate */
        foreach($classUpdates as $update) {

            if($update->isAdded() || $update->isRemoved() || $update->isSignatureChanged()) {
                $hasUpdate = true;
                break;
            }

        }

        $templetFiles = isset($this->properties['templetfiles']) ? unserialize($this->properties['templetfiles']) : array();

        foreach($templetFiles as $filename => $_) {
            $fileStatus = $fileSystem->getFileStatus($filename);

            if($fileStatus === FileSystem :: FILE_REMOVED) {
                $hasUpdate = true;
                unset($templetFiles[$filename]);
            }
        }

        $iterator = new \RecursiveIteratorIterator(
        new RegexRecursiveDirectoryFilterIterator(
        new \RecursiveDirectoryIterator($this->templetPath, \RecursiveDirectoryIterator :: CURRENT_AS_FILEINFO)
        ,'#^[^.].*$#i'
        ,'#^[^.].*\.mgr$#i'));


        foreach($iterator as $filename => $file) {
            if(!$file->isReadable()) {
                continue;
            }

            $fileStatus = $fileSystem->getFileStatus($filename);
            $hasUpdate = $hasUpdate || $fileStatus === FileSystem :: FILE_ADDED || $fileStatus === FileSystem :: FILE_MODIFIED;
            $templetFiles[$filename] = true;
        }

        $this->properties['templetfiles'] = serialize($templetFiles);

        if(!$hasUpdate) {
            return;
        }

        // compile
        $compiler = new TempletCompiler($this->compileRoot, $storageManager, $fileSystem, $architecture);

        $rules = $compiler->compile($this->templetPath);

        $rFile = $this->root . DS . 'rules.mg';

        /*

        // share class dictionary (large body), expect cache to work nicely
        foreach($rules['classDictionary'] as $key => $value) {
        $this->classDictionary[$key] = $value;
        }

        unset($rules['classDictionary']);
        */

        $rules['ruleStacks'] = array();

        foreach($rules['rules'] as $key => $_) {
            $rules['ruleStacks'][$key] = array();
        }

        $implementation = var_export($rules, true);

        file_put_contents($rFile, '<?php return ' . $implementation . ';', FILE_TEXT);
    }

    public function createView($object) {

        return $this;
    }

    public function write($object, Buffer $targetBuffer) {
        if($targetBuffer->isOutputBuffer()) {
            $this->renderDirect($object);
        } else {
            ob_start(function($text) use($targetBuffer) {
                $targetBuffer->write($text);

                return null;
            }, 4096);

            $this->renderDirect($object);

            ob_end_clean();
        }
    }

}

class GrooveException extends Exception {

}

class GrooveContext {

    private $dictionary = null;
    private $properties = null;
    private $filterProvider = null;

    public $dependencies = array();

    public function __construct(Dictionary $dictionary, Properties $properties, FilterProvider $filterProvider) {
        $this->dictionary = $dictionary;
        $this->filterProvider = $filterProvider;
    }

    public function getDictionary() {
        return $this->dictionary;
    }

    public function getFilterProvider() {
        return $this->filterProvider;
    }
}